DUnitWizard is a Delphi plug-in that enables you to rapidly construct DUnit test suites. It generates the code for new DUnit projects, modules and class declarations. Tightly integrated into the Delphi IDE, it uses the environment of the active project for naming and location suggestions, and parses the topmost module to generate new test modules and class declarations.
This wizard is a supplement to the DUnit testing framework. To compile and run the test projects created by the wizard, you must first have installed the DUnit framework source, and it must be accessible through:
Code generation is highly customisable, so the output looks the way you want it:
The goal is for the wizard to do all the grunt work, freeing you to concentrate on testing logic.
DUnitWizard comprises three packages:
The remainder of this document deals with the design-time package, EPCDUnitWizardXX.bpl
Throughout this documentation you will find references to test classes and tested classes. Test classes are the DUnit classes that contain your testing logic. Tested classes are your original code; they are the subject of the DUnit tests. Another commonly used term for tested class is "class under test".
DUnit tests are contained in DUnit test projects. Use the project wizard to create a DUnit dpr file. The test classes may be contained in their own modules (units), or embedded in the same module as the associated tested class.
Use the test module wizard to create a new module and test classes based on an existing module you wish to test. Use the test class wizard to add a new test class to an existing test module. Separate test modules mean we are limited to testing globally-scoped classes (declared in the interface section of modules), and only the published and public methods of those classes, or also protected methods using the well-known Delphi class-cracking hack[2].
Embedding a test class in the same module as the tested class gives us access to everything. We can test module-scoped classes (declared in the implementation section) and private and protected methods of the tested class. Some would argue that you should only test the public interface of a class (ie published and public methods), but how you use the tool is your business. Use the test class wizard to create embedded test classes.
After installing the wizard, the next time you start Delphi you will find a new menu item , DUnit, on the IDE's main menu.
There
are also two new DUnit entries on the New
page of the New Items applet (IDEMenu|File|New...
in D5, IDEMenu|File|New|Other... in D6+). DUnitProject
and DUnit TestModule correspond to the first and
second DUnit menu items respectively.
The project name and path will be prefilled with suggestions based on the
active project in the IDE - specifically, wizard parameters PROJECTNAME
and PROJECTPATH respectively. You
can override these suggestions, or change the format of the parameters to
your liking via the Options dialogue.
Stretch the dialogue sideways to accomodate long paths like this example.
The new width will be remembered. The default value of PROJECTNAME
is to take the current IDE project and append "Tests".
Example:
The current project in the IDE is "customdraw" so the suggested project name becomes "customdrawTests".
The corresponding project filename is shown in a read-only edit field.The
default value of PROJECTPATH is to take the path of the current
project file and create a subdirectory called dunit. Any non-existent
directories in the path will be automatically created.
Example:
The current project path is "C:\ProgramFiles\Borland\Delphi6\Demos\CustomDraw\",
so the suggested project path becomes "C:\ProgramFiles\Borland\Delphi6\Demos\CustomDraw\dunit\"
I strongly suggest that you use project groups for DUnit testing. They allow convenient grouping of your project and associated DUnit test project and easy switching between the two via the Project Manager (MainMenu|View|Project Manager). I would also suggest that you use Delphi's "desktops" feature and dock the project manager so that it is always visible next to the editor pane. Then it is simply a double-click to switch between your project and DUnit test project, and its immediately obvious which project is active.
Clicking the dialogue's "Create" button will create a new DUnit
test project and open the .dpr file in an IDE editor window. For our example,
that code is reproduced below. Note that you can switch between the GUI and
console modes of DUnit testing by commenting or uncommenting the third line
of the file, ie the {$APPTYPE CONSOLE} directive.
// Uncomment the following directive to create a console application // or leave commented to create a GUI application... // {$APPTYPE CONSOLE}program customdrawTests;uses TestFramework {$IFDEF LINUX}, QForms, QGUITestRunner {$ELSE}, Forms, GUITestRunner {$ENDIF}, TextTestRunner;{$R *.RES}begin Application.Initialize;{$IFDEF LINUX} QGUITestRunner.RunRegisteredTests; {$ELSE} if System.IsConsole then TextTestRunner.RunRegisteredTests else GUITestRunner.RunRegisteredTests; {$ENDIF}end.
The format of the generated code can be modifed by editing
the project text template file compiled into the wizard as a text resource.
Now we have a test project, we can start creating test modules. The second item on the DUnit menu, New TestModule... (Alt+U+M), opens a dialogue to configure a test module, based on the topmost unit in the IDE. We will be creating a test module based on zero or more of the globally-scoped classes declared in the topmost (currently selected) unit. The new module will be added to the currently active IDE project, so check that your DUnit test project is active (the bold entry in the IDE Project Manager) before opening this dialogue.
Stretch the dialogue to your liking in either direction and/or move the splitter between the treeviews. The new settings will be remembered.
The Unit name field is prefilled with a suggestion
based on the UNITNAME parameter, which
defaults to the name of the current IDE unit with "Tests" appended.
Of course, you can override the suggestion or change the UNITNAME
parameter via the Options dialogue.
Example:
The currently selected IDE unit is "CustomDrawTreeView", so the suggested test module name becomes "CustomDrawTreeViewTests"
The Unit path field is prefilled with a suggestion
based on the UNITPATH parameter, which
defaults to the path of the current IDE unit with a subdirectory "dunit"
appended. As above, you can override the suggestion or reformat the UNITPATH
parameter. Any non-existent directories in the path will be automatically
created.
Example:
The path of the currently selected IDE unit is: "C:\Program Files\Borland\Delphi6\Demos\CustomDraw\",
so the suggested test module path becomes "C:\Program Files\Borland\Delphi6\Demos\CustomDraw\dunit\"
Next, we have a couple of checkboxes concerning uses
clauses. The first one adds the current IDE unit, CustomDrawTreeView
in our example to the (interface) uses clause in the new
test module. The second checkbox adds the current IDE unit to the "uses"
clause in the current project file (.dpr). The setting of these checkboxes
will be remembered and can also be pre-configured from the Options
dialogue.
METHODNAME
parameter, configurable as above. The default value is the tested method
name with "Test" prepended.Clicking the "Create" button will generate the test module and
open it as a new IDE editor window. The generated code for the example module
is reproduced here. Note that SetUp and TearDown
methods are declared but commented out initially, and that each declared class
is registered with the DUnit test framework in the unit's initialization
section. Place the text cursor (caret) within the class declaration and use
Delphi's code completion feature (Ctrl+Shift+C) to generate the class method
stubs in the implementation section of the unit.
unit CustomDrawTreeViewTests;interfaceuses CustomDrawTreeView, TestFrameWork;type TCustomDrawFormTests = class(TTestCase) privateprotected// procedure SetUp; override; // procedure TearDown; override;published// Test methods procedure TestFormCreate; procedure TestTVCustomDraw; procedure TestTVCustomDrawItem; procedure TestTVGetImageIndex; procedure TestTVGetSelectedIndex; procedure TestExit1Click; procedure TestSelection1Click; procedure TestColor1Click; procedure TestSelectionBackground1Click; procedure TestSolid1Click; procedure TestNone1Click; procedure TestOnCustomDraw1Click; procedure TestOnCustomDrawItem1Click; procedure TestTVExpanded; procedure TestButtonColor1Click; procedure TestButtonSize1Click; procedure TestDrawing1Click; procedure TestColor2Click; procedure TestCustomDraw1Click; procedure TestFont2Click;end;implementationinitializationTestFramework.RegisterTest('CustomDrawTreeViewTests Suite', TCustomDrawFormTests.Suite);end.
The format of the generated code can be modified by editing the relevant text template files compiled into the wizard as a text resource.
You can create a test class declaration for any class, by placing the text cursor anywhere within the class declaration, and selecting New Test Class... (Alt+U+C) from the DUnit menu. A dialogue similar to the test module dialogue appears:

The left treeview displays the selected class declaration. Note that you also
have the option of selecting private methods.
The right treeview is a preview of the test class declaration, and changes dynamically in response to your manipulation of the left treeview. As before, you can preconfigure the selected class visibilities via the Options dialogue - these selections are distinct from the test module choices.
Exit the dialogue by pressing the "Copy to Clipboard" button. Then
paste the new test class declaration wherever you desire, by moving the text
cursor to the insertion point and pressing Ctrl+V or selecting IDEMenu|Edit|Paste.
The generated code for the example is shown here:
type TResourceItemTests = class(TTestCase) private
protected // procedure SetUp; override; // procedure TearDown; override; published // Test methods procedure TestGetName; procedure TestGetResourceList; procedure TestCreateItem; procedure TestIsList; procedure TestOffset; procedure TestSize; procedure TestRawData; procedure TestResTypeStr; procedure TestSaveToFile; procedure TestSaveToStream; end;
You can customise the operation of the DUnitWizard via the Options dialogue and/or by editing the code templates used for code generation, described in the next section.
Let's start with the Options dialogue. Choose Options... (Alt+U+O) from the DUnit menu. The following tabbed dialogue appears:
The three boxes represent each of the three wizards:
All these settings can all be overriden from the respective wizard dialogues.
Clicking on the "Parameters" tab shows the second page, where you
can edit the format of the parameters which substitute for their respective
namesakes in the wizard-generated projects, modules and classes.
To operate this dialogue first select a parameter from the leftmost listbox. the desciption of the selection appears in the panel below the listbox. Then edit the content of the selected parameter in the edit control topmost on the righthand side. This may contain any combination of text and macros selected from the listbox below the edit control.
To insert a macro, place the cursor at the insertion point in the edit control and double-click a macro, or select a macro then press "Insert". The description panel to the right of the listbox updates as the macro selection changes. The last five macros take an argument, which can be any combination of macros and fixed text. Macros can be nested within macros to any level you desire.
You can further customise DUnitWizard by editing the code templates used
for code generation.
The project, test
module and test class wizards all
use code templates. These are parameterised code snippets that are stored
as text resources in the design-time package. The project source template
is stored in the text resource DUNIT_PROJECTSOURCE_TEXT, derived
from ProjectSource.txt. The test module source is stored
in the text resource DUNIT_TESTMODULE_TEXT, derived from TestModule.txt.
The test module source contains references to fixed
parameters which in turn are substituted by other template files. All
these templates can be modified by editing the respective text file. You must
also modify the corresponding file length value (eg ProjectTextLength
for ProjectSource.txt) in DUnitCommon.pas, then recompile and
reinstall the design-time package.
Parameters in the code templates are immediately preceded by a hash character
'#', eg #UNITNAME. They are all case-insensitive, but are upper-case
by convention to highlight their presence. There are both fixed and user-defined
parameters.
USESTESTEDUNIT |
The uses clause entry for the currently selected
IDE unit in the new test module. The line containing this parameter is
replaced by the text resource DUNIT_USESTESTEDUNIT_TEXT from
the design-time package, following substitution for any parameters contained
in the resource. The resource content can be modifed by editing UsesTestedUnit.txt
and recompiling and installing the design-time package. |
TESTCLASSDECLBLOCK |
The test class declaration(s) for the class(es) selected
for testing (the tested classes) from the currently selected IDE
unit. The line containing this parameter is replaced by one instance of
text resource DUNIT_TESTCLASSDECL_TEXT from the design-time
package for each test class, following substitution for any parameters
contained in the resource. The resource content can be modifed by editing
TestClassDecl.txt and recompiling and installing the
design-time package. |
TESTMETHODBLOCK |
Locates the position of the test class method declaration
block within the DUNIT_TESTMODULE_TEXT resource. The line
containing this parameter is replaced by one instance of text resource
DUNIT_TESTMETHODDECL_TEXT from the design-time package for
each test method, following substitution for any parameters contained
in the resource. The resource content can be modifed by editing TestMethodDecl.txt
and recompiling and installing the design-time package. |
TESTSUITEREGBLOCK |
The test class registrations with DUnit in the new test
module. The line containing this parameter is replaced by one instance
of text resource DUNIT_TESTSUITEREG_TEXT from the design-time
package for each test class, following substitution for any parameters
contained in the resource. The resource content can be modifed by editing
TestSuiteReg.txt and recompiling and installing the design-time
package. |
TESTEDUNITNAME |
The name of the currently selected IDE unit (no path or extension) |
TESTEDUNITPATH |
The file system absolute path of the directory containing the currently selected IDE unit (includes trailing directory delimiter character) |
The user-defined parameters in turn are defined by templates, configured
via the Options dialogue. These templates can
include macros. They are case-insensitive, but upper-cased by convention,
and identified by a prefixed dollar character '$', eg $CURRENTUNIT
TESTEDCLASSNAME |
The name of a class from the currently selected IDE unit
that is to be tested (no .pas extension). Note: This macro is only meaningful in the context of the definition of the CLASSNAME parameter. |
TESTEDMETHODNAME |
The name of a method from a tested class. Note: This macro is only meaningful in the context of the definition of the METHODNAME parameter. |
CURRENTUNIT |
The absolute file specification for the currently selected IDE unit (includes path and filename) |
CURRENTPROJECT |
The absolute file specification for the currently selected IDE project (includes path and filename for the Delphi project file, .dpr) |
PROJECTGROUP |
The absolute file specification for the currently selected IDE project group (includes path and filename for the Delphi project group file, .bpg) |
FILEPATH() |
This macro extracts the path (including trailing directory delimiter) from the filename expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting. |
FILENAME() |
This macro function extracts the filename (including any extension) from the file specification expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting. |
FILESTEM() |
This macro function extracts the filestem (filename excluding extension) from the file specification expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting. |
FILEEXT() |
This macro function extracts the filename extension (including leading period) from the file specification expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting. |
ENVIROVAR() |
This macro returns the current value of the environment variable expression passed as the argument. The expression can be any combination of literal text and other macros, with unlimited nesting. |
The entry point for any design-time package is a global procedure named Register
(case-sensitive). This is contained in XPDUnitWizard.pas.
This procedure registers an instance of IOTAWizard, implemented
by TXPDUnitWizard. This class creates the project, module, class
and menu wizards mentioned earlier, and manages the registration and deregistration
of these wizards with the IDE.
I recommend the use of a container wizard like this for OTA plug-ins. Interfaces
registered via ToolsAPI.RegisterPackageWizard() must have a reference
count of one to be destroyed by the IDE; otherwise, you will very likely get
access violations whenever your design-time package is unloaded. Many wizards
will implement more than one interface and will consequently have a reference
count of two or more at times. Using a container wizard for IDE registration
avoids this problem.
The project wizard is declared in XPDUnitProjectWizard.pas.
The class TProjectWizard implements the necessary OTA wizard
interfaces. The method TProjectWizard.Execute pops up the project
dialogue. The dialogue, implemented in XPDUnitProject.pas,
loads the wizard parameters and persisted settings in TXPDUnitProjectForm.FormCreate.
The wizard parameters are evaluated for the active project in TXPDUnitProjectForm.FormShow.
If the 'Create' button is clicked, a new module is created. The class TProjectCreator
implements the OTA module interfaces. TProjectCreator.NewProjectSource
returns an interface, IOTAFile, implemented by TProjectSource,
which provides the source for the dpr file. TProjectSource.Create
extracts the template resource from the package, and TProjectSource.GetSource
makes the substitutions based on the user's choices from the project dialogue.
The test module wizard is declared in XPDUnitTestModuleWizard.pas.
The class TTestModuleWizard implements the necessary OTA wizard
interfaces. The method TTestModuleWizard.Execute pops up the
test module dialogue. The dialogue, implemented in XPDUnitTestModule.pas,
loads the current wizard parameters, persisted settings and creates a module
parser and filters in TXPDUnitProjectForm.FormCreate. The module
parser is implemented in XPTestedUnitParser.pas, the parser filters
in XPParserFilters.pas. There is a parser filter for each of the treeviews
in the dialogue. The wizard parameters are evaluated, the unit parsed, and
the resultant parse tree filtered for the active module in the IDE in TXPDUnitTestModuleForm.FormShow.
If the 'Create' button is clicked, a new module is created. The class TTestModuleCreator
implements the OTA module interfaces. The filtered test class parser tree
is passed into TTestModuleCreator. TTestModuleCreator.NewImplSource
returns an interface, IOTAFile, implemented by TTestModuleSource,
which provides the source for the new pas file. TTestModuleSource.Create
creates an IXPDUnitTextTemplates interface, implemented in XPTextTemplates.pas,
to extract the template resource from the package and make the substitutions
based on the user's choices from the test module dialogue.
The test class wizard is declared in XPDUnitTestClassWizard.pas.
The class TTestClassWizard implements the necessary OTA wizard
interfaces. The method TTestClassWizard.Execute pops up the test
module dialogue. The dialogue, implemented in XPDUnitTestClass.pas,
loads the current wizard parameters, persisted settings and creates a module
parser and filters in TXPDUnitTestClassForm.FormCreate. The same
parser as the test module wizard is used again, but different filters to extract
only the current class declaration (enclosing the cursor postion in the current
IDE unit). If the 'Copy to Clipboard' button is clicked, a new test class
declaration is created and copied to the clipboard. TTestClassWizard.Execute
creates an IXPDUnitTextTemplates interface, as used by the test
module wizard, to extract the template resource from the package and make
the substitutions based on the user's choices from the test class dialogue.
The menu wizard is declared in XPDUnitMenuWizard.pas. The
class TXPDUnitMenuWizard implements a basic IOTAWizard
interface, taking references to the project, test module and test class wizards
as arguments to its constructor. TXPDUnitMenuWizard.Create creates
the DUnit menu, wires up all the other wizards to their respective menu items
and also wires up the Options dialogue and HTML documentation. The destructor
TXPDUnitMenuWizard.Destroy reverses these operations.
Please feel free to ask questions via the EPC support web page.
Q: The wizard creates the test modules in a subdirectory below the tested module called 'dunit'. I keep them in the same directory as the tested modules. How can I set this up?
A: Choose IDEMenu|DUnit|Options... Click the Parameters
tab and select the UNITPATH parameter. Change the default value
$FILEPATH($CURRENTUNIT)dunit\ to $FILEPATH($CURRENTUNIT)
Q: All my source modules and project files are in a directory called 'Source'. All my test modules and test project files are in a directory at the same level called 'Tests'. How can I set this up?
A: Choose IDEMenu|DUnit|Options... Click the Parameters
tab and select the PROJECTPATH parameter. Change the default
value $FILEPATH($CURRENTPROJECT)dunit\ to $FILEPATH($CURRENTPROJECT)..\Tests\.
Select the UNITPATH parameter. Change the default
value $FILEPATH($CURRENTUNIT)dunit\ to $FILEPATH($CURRENTPROJECT)..\Tests\
Q: I prefer to use a prefix of TEST_ for all my test method names. How can I set this up?
A: Choose IDEMenu|DUnit|Options... Click the Parameters
tab and select the METHODNAME parameter. Change the default value
Test$TESTEDMETHODNAME to TEST_$TESTEDMETHODNAME
Q: I always test protected methods as well as published and public. How can I set this up so I don't have to select the protected node each time in the 'New TestModule dialogue'?
A: Choose IDEMenu|DUnit|Options... Click the Behaviour tab and tick the Add PROTECTED method tests checkbox in the New TestModule group box.
Footnotes:
[1] (XX denotes the Delphi version: XX=50: Delphi 5, XX=60: Delphi 6, XX=70: Delphi 7)
[2]
Class cracking example:
There is a class TSample in a unit Sample.pas. You can access the protected
methods of TSample from another unit, say Test.pas, by declaring an empty
subclass of TSample in Test.pas, say TCrackedSample, and casting instances
of TSample to TCrackedSample.
In Sample.pas: |
In Test.pas: |